using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class InputManager : MonoBehaviour
{
    public enum KeyType
    {
        Down,
        Up
    }

    private Action m_bufferActions;
    private readonly Dictionary<KeyCode, Action<KeyType>> m_dict = new Dictionary<KeyCode, Action<KeyType>>();
    private readonly Dictionary<int, Action<KeyType>> m_dictMouse = new Dictionary<int, Action<KeyType>>();
    private readonly Dictionary<KeyCode, KeyType> m_keyActions = new Dictionary<KeyCode, KeyType>();
    private readonly Dictionary<int, KeyType> m_mouseActions = new Dictionary<int, KeyType>();
    private readonly List<KeyCombination> m_combinations = new List<KeyCombination>();

    public void Bind(KeyCode key, Action<KeyType> action)
    {
        m_bufferActions += () =>
        {
            if (!m_dict.ContainsKey(key))
            {
                m_dict.Add(key, action);
            }
            else
            {
                m_dict[key] += action;
            }
        };
    }

    public void BindMouse(int id, Action<KeyType> action)
    {
        m_bufferActions += () =>
        {
            if (!m_dictMouse.ContainsKey(id))
            {
                m_dictMouse.Add(id, action);
            }
            else
            {
                m_dictMouse[id] += action;
            }
        };
    }

    public void Remove(KeyCode key, Action<KeyType> action)
    {
        m_bufferActions += () =>
        {
            if (!m_dict.ContainsKey(key)) return;
            m_dict[key] -= action;
        };
    }

    public void RemoveMouse(int id, Action<KeyType> action)
    {
        m_bufferActions += () =>
        {
            if (!m_dictMouse.ContainsKey(id)) return;
            m_dictMouse[id] -= action;
        };
    }

    private void Update()
    {
        m_bufferActions?.Invoke();
        m_bufferActions = null;

        //Not efficient. Specific implementations for specific occasions instead are suggested.
        foreach (KeyCombination combination in m_combinations)
        {
            if (combination.keyCodes.All(key =>
                    m_keyActions.ContainsKey(key.code) || Input.GetKeyDown(key.code) || Input.GetKey(key.code))
                && combination.mouseIds.All(mouse =>
                    m_mouseActions.ContainsKey(mouse.id) || Input.GetMouseButtonDown(mouse.id) ||
                    Input.GetMouseButton(mouse.id)))
            {
                if (combination.activated) continue;
                combination.action.Invoke();
                combination.activated = true;
                continue;
            }

            if (!combination.activated) continue;

            combination.activated = false;
        }

        foreach (var pair in m_dict)
        {
            if (Input.GetKeyDown(pair.Key))
            {
                DebugManager.Log($"{pair.Key} Key Down!", -10);
                pair.Value?.Invoke(KeyType.Down);
            }

            if (Input.GetKeyUp(pair.Key))
            {
                DebugManager.Log($"{pair.Key} Key Up!", -10);
                pair.Value?.Invoke(KeyType.Up);
            }

            if (m_keyActions.ContainsKey(pair.Key))
            {
                DebugManager.Log($"{pair.Key} Virtual Key {m_keyActions[pair.Key].ToString()}!", -10);
                pair.Value?.Invoke(m_keyActions[pair.Key]);
            }
        }

        foreach (var pair in m_dictMouse)
        {
            if (Input.GetMouseButtonDown(pair.Key))
            {
                DebugManager.Log($"{pair.Key} Mouse Key Down!", -10);
                pair.Value?.Invoke(KeyType.Down);
            }

            if (Input.GetMouseButtonUp(pair.Key))
            {
                DebugManager.Log($"{pair.Key} Mouse Key Up!", -10);
                pair.Value?.Invoke(KeyType.Up);
            }

            if (m_mouseActions.ContainsKey(pair.Key))
            {
                DebugManager.Log($"{pair.Key} Virtual Mouse Key {m_mouseActions[pair.Key].ToString()}!", -10);
                pair.Value?.Invoke(m_mouseActions[pair.Key]);
            }
        }

        m_keyActions.Clear();
        m_mouseActions.Clear();
    }

    public KeyCombination Bind(Action action, params KeyCode[] keyCodes)
    {
        return BindCombination(new KeyCombination(keyCodes.ToList(), new List<int>(), action));
    }

    public KeyCombination Bind(Action action, IEnumerable<KeyCode> keyCodes)
    {
        return BindCombination(new KeyCombination(keyCodes.ToList(), new List<int>(), action));
    }

    public KeyCombination BindMouse(Action action, params int[] ids)
    {
        return BindCombination(new KeyCombination(new List<KeyCode>(), ids.ToList(), action));
    }

    public KeyCombination BindMouse(Action action, IEnumerable<int> ids)
    {
        return BindCombination(new KeyCombination(new List<KeyCode>(), ids.ToList(), action));
    }

    public KeyCombination BindAll(Action action, IEnumerable<KeyCode> keyCodes, IEnumerable<int> mouseIds)
    {
        return BindCombination(new KeyCombination(keyCodes.ToList(), mouseIds.ToList(), action));
    }

    public void Remove(KeyCombination combination)
    {
        m_combinations.Remove(combination);
    }

    private KeyCombination BindCombination(KeyCombination combination)
    {
        m_bufferActions += () => m_combinations.Add(combination);
        return combination;
    }

    public void SendVirtualKey(KeyCode code, KeyType type)
    {
        m_bufferActions += () =>
        {
            if (m_keyActions.ContainsKey(code))
            {
                DebugManager.LogWarning(
                    $"Virtual Key Action(Code: {code}, Type: {type}) conflicts with existing Virtual Key Action(Code: {code}, Type: {m_keyActions[code]}). Action will be ignored.");
                return;
            }

            m_keyActions.Add(code, type);
        };
    }

    public void SendVirtualMouseKey(int code, KeyType type)
    {
        m_bufferActions += () =>
        {
            if (m_mouseActions.ContainsKey(code))
            {
                DebugManager.LogWarning(
                    $"Virtual Mouse Action(Code: {code}, Type: {type}) conflicts with existing Virtual Mouse Action(Code: {code}, Type: {m_mouseActions[code]}). Action will be ignored.");
                return;
            }

            m_mouseActions.Add(code, type);
        };
    }

    public class KeyCombination
    {
        public List<Key> keyCodes;
        public List<Mouse> mouseIds;
        public bool activated;
        public Action action;

        public KeyCombination(List<KeyCode> keyCodes, List<int> mouseIds, Action action)
        {
            this.keyCodes = keyCodes.Select(keyCode => new Key(keyCode)).ToList();
            this.mouseIds = mouseIds.Select(mouseId => new Mouse(mouseId)).ToList();
            this.action = action;
            activated = false;
        }

        public class Key
        {
            public KeyCode code;

            public Key(KeyCode code)
            {
                this.code = code;
            }
        }

        public class Mouse
        {
            public int id;

            public Mouse(int id)
            {
                this.id = id;
            }
        }
    }
}